#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#
# TSys - Build and integration tests management.
# Copyright (C) 2007 Instituto Nokia de Tecnologia
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

from __future__ import with_statement

import logging as log

import sys
import os
import re
import urllib
import shlex
import shutil
import subprocess
import time
import calendar
import shelve

from cStringIO import StringIO

# Utils
def expand_all(filename):
    return os.path.expanduser(os.path.expandvars(filename))

def join(filename):
    if isinstance(filename, list):
        return os.path.join(*(expand_all(f) for f in filename))
    else:
        return expand_all(filename)

def split(path):
    return expand_all(path).split(os.sep)

def exists(filename):
    return os.path.exists(join(filename))

def command_output(runcmd_return):
    res = ""
    for res in runcmd_return:
        sys.stdout.write(res)
        if res and res[-1] != '\n':
            sys.stdout.write('\n')

def split_args(args):
    try:
        splitted_args = shlex.split(args)
    except AttributeError:
        splitted_args = args

    return splitted_args


# Downloader
class _URLOpener(urllib.FancyURLopener):
    def http_error_206(*args, **kwargs):
        pass
    def http_error_404(*args, **kwargs):
        raise IOError("Page not found.")

class DownloadError(Exception):
    pass

class Download(object):
    def __init__(self, uri, path=None):
        self.uri = uri
        self.path = path
        self.urlclass = _URLOpener()

        self.remote_timestamp = int(time.time())
        self.local_timestamp = self.remote_timestamp

        self._size = None
        self.total = None
        self.buffer = None

        self.download = False

    def _get_size(self):
        if self._size is not None:
            return self._size

        if self.buffer is None and self.path is None:
            self._size = 0
        else:
            try:
                self._size = len(self.buffer.getvalue())
            except AttributeError:
                self._size = self.path.size

        return self._size
    def _set_size(self, value):
        self._size = value
    size = property(_get_size, _set_size)

    def init_buffer(self, reset=False):
        if self.buffer is not None and not reset:
            return

        if self.path is None:
            self.buffer = StringIO()
        elif self.path.exists and not reset:
            self.buffer = self.path.open("ab")
        else:
            self.buffer = self.path.open("wb")

    def done(self, update=False):
        if not update and self.total is not None and self.size >= self.total:
            self.finish()
            return True

        try:
            page = self.urlclass.open(self.uri)
            try:
                self.total = int(page.headers['Content-Length'])
                ptime = time.strptime(page.headers["Last-Modified"], "%a, %d %b %Y %H:%M:%S %Z")
                self.remote_timestamp = calendar.timegm(ptime)
                self.local_timestamp = self.path.mtime
            except (KeyError, IOError):
                self.remote_timestamp = int(time.time())
                self.local_timestamp = self.remote_timestamp
            finally:
                page.close()

            if update and self.local_timestamp < self.remote_timestamp:
                self.init_buffer(reset=True)
                self.path.mtime = self.remote_timestamp
            else:
                if self.total is not None and self.size >= self.total:
                    self.finish()
                    return True

                self.init_buffer()
                if self.size and self.size < self.total:
                    self.urlclass.addheader("Range", "bytes=%s-" % (self.size,))

            page = self.urlclass.open(self.uri)
        except IOError:
            if self.path:
                self.path.mtime = self.remote_timestamp
            return False

        while True:
            data = page.read(8192)
            if not data:
                break

            self.download = True
            self.buffer.write(data)
            self.size += len(data)

        if self.total is not None and self.size >= self.total:
            self.finish()
            return True

        if self.path:
            self.path.mtime = self.remote_timestamp

        return False

    def run(self, tries=1, update=False):
        for i in xrange(tries):
            if self.done(update=update):
                break
        else:
            raise DownloadError("Exceed the maximum number of tries.")
        return self.download

    def finish(self):
        if self.path is not None and self.buffer is not None:
            self.buffer.close()
            self.buffer = None
            if self.remote_timestamp:
                self.path.mtime = self.remote_timestamp

    @property
    def content(self):
        try:
            content = self.buffer.getvalue()
            self.buffer.close()
            self.buffer = None
        except AttributeError:
            try:
                content = self.path.content
            except IOError:
                content = ""
        return content


# File and Dir
class File(object):
    def __init__(self, name, parent=None):
        self.name = name
        if type(parent) == File:
            raise ValueError("File cannot contain files.")
        self.parent = parent

    def __repr__(self):
        return "<FILE '%s'>" % (self.name,)

    def __str__(self):
        return self.path

    @property
    def path(self):
        if not self.parent:
            return self.name
        return "%s%s%s" % (self.parent.path, os.sep, self.name)

    def _get_mtime(self):
        try:
            stat = os.stat(self.path)
            return stat.st_mtime
        except OSError:
            self.create()
            return time.time()

    def _set_mtime(self, new_time):
        os.utime(self.path, (new_time, new_time))
    mtime = property(_get_mtime, _set_mtime)

    @property
    def exists(self):
        return os.path.exists(self.path)

    @property
    def size(self):
        try:
            return os.path.getsize(self.path)
        except OSError:
            return 0

    @property
    def content(self):
        with self.open() as f:
            return f.read()

    def create(self):
        with self.open("w"):
            pass

    def remove(self):
        if self.exists:
            os.remove(self.path)

    def open(self, mode="r"):
        if not self.exists and self.parent:
            self.parent.create()
        return open(self.path, mode)

    def write(self, content):
        with self.open("w") as f:
            f.write(content)

    def append(self, content):
        with self.open("a") as f:
            f.write(content)

class Dir(File):
    def __init__(self, name, parent=None):
        super(Dir, self).__init__(name, parent)
        self._content = []

    def __repr__(self):
        return "<DIR '%s'>" % (self.name,)

    def create(self):
        if os.path.exists(self.path):
            if os.path.isdir(self.path):
                return
        try:
            os.makedirs(self.path)
        except OSError, ex:
            log.warning(str(ex))

    def remove(self):
        err = []
        def err_handler(*args):
            err.append(args)

        if self.exists:
            shutil.rmtree(self.path, onerror=err_handler)

        return err

    @property
    def content(self):
        return self._content

    def append(self, item):
        self.content.append(item)
        item.parent = self

rootdir = Dir("")

def create_path(path):
    if not path:
        raise ValueError("Invalid path")

    if path in [".", ".."]:
        path += os.sep

    splitted_path = split(path)
    tail = len(splitted_path) - 1

    last_element = None
    for pos, name in enumerate(splitted_path):
        if not name and pos not in (0, tail): # filter
            continue

        if pos == 0 and not name: # rootdir
            last_element = rootdir
            continue

        if pos < tail:
            cls = Dir
        elif name:
            cls = File
        else:
            element = last_element
            break

        element = cls(name, last_element)
        last_element = element

    return element

# Exceptions
class OSServiceError(Exception):
    pass

# Abstract OS Class
class OS(object):
    name = "Generic"

    def __init__(self):
        self.sysadmin = False

    def enable_sysadmin(self):
        self.sysadmin = True

    def disable_sysadmin(self):
        self.sysadmin = False

    def runcmd(self, cmd, cwd=None):
        args = split_args(cmd)

        try:
            cwd = cwd.path
        except AttributeError:
            pass

        process = subprocess.Popen(
            args=args,
            cwd=cwd,
            universal_newlines=True,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

        command = self.build_prompt(cwd, args)
        return command, process.stdout.read(), process.stderr.read()

    def build_prompt(self, cwd, cmd):
        return ""

class Unix(OS):
    def build_prompt(self, cwd, cmd):
        if cwd is None:
            cwd = ""

        if self.sysadmin:
            prompt = "#"
            del cmd[0]
        else:
            prompt = "$"

        return "%s%s %s" % (cwd, prompt, ' '.join(cmd))

class Linux(Unix):
    name = "Linux"
    def runcmd(self, cmd, cwd=None):
        if self.sysadmin:
            cmd = split_args(cmd)
            cmd.insert(0, "sudo")
        return super(Unix, self).runcmd(cmd, cwd)

class Debian(Linux):
    name = "Debian"
    _package_cache = {}

    def parse_package_name(self, package_name):
        splitted_name = package_name.split("_")
        splitted_name[-1] = splitted_name[-1].split(".")[0]
        return tuple(splitted_name)

    def get_installed_packages(self, *package_names, **kwargs):
        dpkg_dir = kwargs.get("dpkg_dir", Dir("/var/lib/dpkg"))
        status_file = File("status", dpkg_dir)

        pack_names = list(package_names)
        ret = []

        for package_name in package_names:
            if package_name in Debian._package_cache:
                status = Debian._package_cache[package_name]['status']
                version = Debian._package_cache[package_name]['version']
                ret.append((package_name, version, status))
                pack_names.remove(package_name)

        if not pack_names:
            return tuple(ret)

        # populate cache
        package_name = ""
        with status_file.open() as f:
            for line in f:
                line = line.strip()
                if line.startswith("Package: "):
                    package_name = line.split(": ")[1]
                    if package_name in Debian._package_cache:
                        package_name = ""
                        continue
                    Debian._package_cache[package_name] = {
                        'version': '',
                        'status': ()
                    }
                elif line.startswith("Status: ") and package_name:
                    status = tuple(line.split(": ")[1].split())
                    Debian._package_cache[package_name]['status'] = status
                elif line.startswith("Version: ") and package_name:
                    version = line.split(": ")[1]
                    Debian._package_cache[package_name]['version'] = version
                else: # ignore others
                    pass

        # check the rest of the packages
        for package_name in pack_names:
            if package_name in Debian._package_cache:
                status = Debian._package_cache[package_name]['status']
                version = Debian._package_cache[package_name]['version']
                ret.append((package_name, version, status))

        return tuple(ret)

    def install_package_files(self, *package_files):
        package_list = []
        for package_file in package_files:
            name, version, arch = self.parse_package_name(package_file.name)
            installed = self.get_installed_packages(name)
            if not installed or \
               ('purge' in installed[0][-1]) or \
               ('deinstall' in installed[0][-1]) or \
               ('not-installed' in installed[0][-1]):
                package_list.append(str(package_file))

        if not package_list:
            return ("","", "")

        return self.runcmd(["dpkg", "-i"] + package_list)

    def install_system_packages(self, *packages):
        return self.runcmd(["apt-get", "install", "-y"] + list(packages))

class Ubuntu(Debian):
    name = "Ubuntu"

class OSService(object):
    def __init__(self):
        self.oss = {}
        self._current = None

    def register(self, os):
        if not hasattr(os, "name") and \
            not issubclass(os, _OS):
            raise OSServiceError("Invalid OS class.")
        self.oss[os.name] = os()

    @property
    def current(self):
        if self._current is None:
            raise OSServiceError("You need select the current OS.")
        return self._current

    def select(self, os_name):
        self._current = self.oss.get(os_name, None)

    def discover(self):
        try:
            with open("/etc/lsb-release") as rf:
                for line in rf:
                    if "=" not in line:
                        continue

                    key, value = line.strip().split("=")

                    if key == "DISTRIB_ID":
                        os = value
                        break
        except IOError:
            os = None

        if os in self.oss:
            self.select(os)
        else:
            raise OSServiceError("Cannot discover the OS.")

    # Dispatch
    def __getattr__(self, name):
        try:
            return getattr(super(OSService, self), name)
        except AttributeError:
            return getattr(self._current, name)


oss = OSService()
oss.register(Ubuntu)
oss.register(Debian)
oss.register(Linux)
oss.register(Unix)

# Test
if __name__ == "__main__":
    # Dir
    root_dir = create_path("/tmp/teste/")
    data_dir = Dir("data", root_dir)
    download_dir = Dir("files", root_dir)
    chroot_dir = Dir("root", root_dir)

    # Files
    debootstrap_script_file = File("mamona", download_dir)
    filelist = File("filelist", data_dir)
    qemu_packages = [
        File("qemu_0.9.0-3_i386.deb", download_dir),
        File("qemu-binfmt_0.9.0-3_i386.deb", download_dir),
        File("qemu-user-static_0.9.0-3_i386.deb", download_dir),
    ]

    # URLs
    qemu_repo_url = "http://osmtc.indt.org/repository/mamona/qemu/%s"
    debootstrap_script_url = "http://osmtc.indt.org/repository/mamona/debootstrap/mamona"
    mamona_repo_url = "http://osmtc.indt.org/repository/mamona"

    # Commands
    debootstrap_command = [
        "debootstrap", "--arch", "arm", "mamona",
        str(chroot_dir), mamona_repo_url, str(debootstrap_script_file)
    ]

    create_filelist_command = [
        "find", "-fprintf",
        filelist.path, r"""'"%P"\t"%y"\t"%U"\t"%G"\t"%m"\t"%t"\n'"""
    ]

    # Action!
    oss.discover()

    def get_status():
        status_file = File("status.db", data_dir)
        if not status_file.exists:
            if status_file.parent:
                status_file.parent.create()
            return shelve.open(status_file.path, 'c')
        else:
            return shelve.open(status_file.path)
    status = get_status()

    # TODO remove
    #status["qemu_installed"] = False
    status["system_packages_installed"] = False
    #status["reconstruct_root"] = False
    #status["create_filelist"] = False

    def install_qemu(status, qemu_packages):
        if status.get("qemu_installed"):
            return

        print "REPORT: installing qemu packages..."
        download_dir.create()
        for qemu_package in qemu_packages:
            need_update = Download(qemu_repo_url % (qemu_package.name,), qemu_package).run(tries=3, update=True)
            if need_update:
                oss.enable_sysadmin()
                command_output(oss.install_package_files(qemu_package))
                oss.disable_sysadmin()

        print "REPORT: qemu packages installed."
        status["qemu_installed"] = True

    def install_system_packages(status):
        if status.get("system_packages_installed"):
            return

        oss.enable_sysadmin()
        command, stdout, stderr = oss.install_system_packages(
            "libc6",
            "libasound2",
            "libncurses5",
            "libsdl1.2debian",
            "zlib1g",
            "vgabios",
            "bochsbios",
            "binfmt-support",
            "dpkg-dev",
            "debootstrap",
        )
        oss.disable_sysadmin()

        splitted_output = stdout.strip().split(os.linesep)
        summary_pattern = re.compile(r"^([0-9]+) upgraded, ([0-9]+) newly installed, "
                                      "([0-9]+) to remove and ([0-9]+) not upgraded")
        for line in splitted_output:
            install_summary = summary_pattern.match(line)
            if install_summary:
                install_summary = list(install_summary.groups())
                break
        else:
            install_summary = []

        # Check the apt-get summary:
        # 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded
        if install_summary.count("0") != 4:
            command_output((command, stdout, stderr))
        else:
            command_output((command, "", ""))

        status["system_packages_installed"] = True

    def create_chroot(status, script_url, script_file, chroot_dir):
        status["reconstruct_root"] = Download(debootstrap_script_url, debootstrap_script_file).run(tries=3, update=True)
        if not status["reconstruct_root"] and chroot_dir.exists:
            return

        oss.enable_sysadmin()
        command_output(oss.runcmd(debootstrap_command))
        oss.disable_sysadmin()

        status["reconstruct_root"] = False
        status["create_filelist"] = True

    def create_filelist(status, chroot_dir, filelist):
        if filelist.exists:
            return
        command_output(oss.runcmd(create_filelist_command, chroot_dir))


    def mount_chroot(status, chroot_dir):
        if status.get("chroot_mounted"):
            return

        proc_dir = Dir("proc", chroot_dir)
        dev_dir = Dir("dev", chroot_dir)

        oss.enable_sysadmin()
        command_output(oss.runcmd(["mount", "-t", "proc", "proc", proc_dir.path]))
        command_output(oss.runcmd(["mount", "--bind", "/dev", dev_dir.path]))
        oss.disable_sysadmin()

        status["chroot_mounted"] = True

    def umount_chroot(status, chroot_dir):
        if not status.get("chroot_mounted"):
            return

        proc_dir = Dir("proc", chroot_dir)
        dev_dir = Dir("dev", chroot_dir)

        oss.enable_sysadmin()
        command_output(oss.runcmd(["umount", proc_dir.path]))
        command_output(oss.runcmd(["umount", dev_dir.path]))
        oss.disable_sysadmin()

        status["chroot_mounted"] = False


    def enter_chroot():
        # return inside environment
        pass

    install_system_packages(status)
    install_qemu(status, qemu_packages)
    create_chroot(status, debootstrap_script_url, debootstrap_script_file, chroot_dir)
    create_filelist(status, chroot_dir, filelist)
    mount_chroot(status, chroot_dir)

    #

    umount_chroot(status, chroot_dir)


# vim:ts=4:sw=4:tw=80:et:sm:ai:si
